package cn.wjssl.aop;

import cn.wjssl.infrastructure.persistent.redis.IRedisService;
import cn.wjssl.types.annotations.DCCValue;
import cn.wjssl.types.annotations.RateLimiterAccessInterceptor;
import cn.wjssl.types.common.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;


/**
 * 限流 aop
 */
@Slf4j
@Aspect
@Component
public class RateLimiterAOP {

    // 限流开关, 在zooKeeper中动态配置
    @DCCValue("rateLimiterSwitch:close")
    private String rateLimiterSwitch;

    @Resource
    private IRedisService redisService;

    // 定义切点
    @Pointcut("@annotation(cn.wjssl.types.annotations.RateLimiterAccessInterceptor)")
    public void aopPoint() {}

    // 拦截后通知
    @Around("aopPoint() && @annotation(rateLimiterAccessInterceptor)")
    public Object doRouter(ProceedingJoinPoint jp, RateLimiterAccessInterceptor rateLimiterAccessInterceptor) throws Throwable {
        // 1. 先校验限流规则开关, 如果没开启, 那就正常执行原方法
        if (StringUtils.isBlank(rateLimiterSwitch) || "close".equals(rateLimiterSwitch)) {
            return jp.proceed();    // 正常执行原方法
        }

        // 2. 校验拦截方法上的自定义注解是否配置完毕
        String key = rateLimiterAccessInterceptor.key();    // 被拦截方法注解上定义的拦截字段 --> userId
        if (StringUtils.isBlank(key)) {
            throw new RuntimeException("annotation RateLimiter uId is null！");
        }

        // 3. 获取拦截方法入参中, 对应的拦截字段
        String keyAttr = getAttrValue(key, jp.getArgs());   // 被拦截方法中, 入参传入的拦截字段的值  --> lxd
        log.info("aop attr {}", keyAttr);

        /**
         * 4. 黑名单拦截, 如果在注解中配置了黑名单拦截次数,以及拦截字段, 那先进行黑名单拦截限流的判断
         * 从redis中查询, 是否有该用户的限流黑名单key
         * 如果有的话就拦截, 不执行原方法draw(), 执行注解中配置的拦截方法
         */
        String cacheRateLimiterBlackListKey = Constants.RedisKey.RATE_LIMITER_BLACK_LIST_KEY + keyAttr;
        RAtomicLong rAtomicLong = redisService.getRAtomicLong(cacheRateLimiterBlackListKey);// redis中获取限流黑名单计数器
        if (!keyAttr.equals("all") && rateLimiterAccessInterceptor.blacklistCount() != 0) {     // 如果在注解中不是默认, 而是配置了限流黑名单配置
            boolean exists = rAtomicLong.isExists();
            if (exists && rAtomicLong.get() > rateLimiterAccessInterceptor.blacklistCount()) {    // 如果redis中有限流黑名单计数器, 并且次数已经到黑名单次数了
                log.info("限流-黑名单拦截(24h)：{}", keyAttr);
                return fallbackMethodResult(jp, rateLimiterAccessInterceptor.fallbackMethod());     // 不执行draw(), 执行在注解中配置的fallbackMethod的方法
            }
        }
        /**
         * 5. 限流拦截
         * 获取redis的限流器, 如果没有的话就创建, trySetRate(), 并且把注释中配置的令牌数(也就是每秒请求次数), 设置给这个限流器
         * 然后判断是否能拿到令牌(这里每秒限制一个请求), tryAcquire()
         *      * 第一个请求进来可以拿到令牌, 返回true
         *      * 第二个请求进来就拿不到令牌了, 被限流, 返回false
         * 如果拿不到令牌了, 说明被限流, 如果配置了限流黑名单配置, 那就设置限流黑名单的计数器和过期时间, 并返回限流方法, 不执行原方法draw()
         */
        String cacheRateLimiterKey = Constants.RedisKey.RATE_LIMITER_KEY + keyAttr;
        RRateLimiter rRateLimiter = redisService.getRRateLimiter(cacheRateLimiterKey);
        if (!rRateLimiter.isExists()) {     // 限流器不存在就创建
            /**
             * RateType.OVERALL 为全局限流, 所有客户端都共享这个限流器
             * 第二个参数: 每秒请求次数
             * 第三个参数: 限流器时长
             * 第四个参数: 时长单位
             */
            rRateLimiter.trySetRate(RateType.OVERALL, (long) rateLimiterAccessInterceptor.permitsPerSecond(), 1, RateIntervalUnit.SECONDS);
        }
        if (!rRateLimiter.tryAcquire()) {   // 如果拿不到令牌, 被限流
            if (!keyAttr.equals("all") && rateLimiterAccessInterceptor.blacklistCount() != 0) {     // 如果有黑名单配置
                rAtomicLong.expire(Duration.ofHours(24));   // 设置限流黑名单过期时间
                rAtomicLong.incrementAndGet();  // 限流黑名单计数器 + 1
                log.info("限流-超频次拦截：{}", keyAttr);
                return fallbackMethodResult(jp, rateLimiterAccessInterceptor.fallbackMethod());     // 如果redis中有限流黑名单计数器, 并且次数已经到黑名单次数了
            }
        }
        // 如果没有被拦截, 就执行原方法
        return jp.proceed();
    }

    /**
     * 调用用户配置的回调方法，当拦截后，返回回调结果。
     */
    private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        MethodSignature methodSignature = (MethodSignature) jp.getSignature();    // 获取被拦截方法的方法前面
        Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());   // 获取被拦截方法的那个类, 从那个类中获取传入的方法名的方法
        return method.invoke(jp.getThis(), jp.getArgs());   // 执行该方法, 并把被拦截方法的入参传入其中
    }

    // 从拦截方法所有入参 args 中, 获取 attr 对应的字段的值
    public String getAttrValue(String attr, Object[] args) {
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (StringUtils.isNotBlank(filedValue)) {
                    break;
                }
                filedValue = String.valueOf(this.getValueByName(attr, arg));
            } catch (Exception e) {
                log.error("获取拦截字段值失败 attr：{}", attr, e);
            }
        }
        return filedValue;
    }

    // 从对象中, 根据字段名获取字段的值
    public Object getValueByName(String name, Object arg) {
        try {
            Field field;
            try{
                field = arg.getClass().getDeclaredField(name);  // 根据名字, 从获取该对象中对应的字段
            }catch (NoSuchFieldException e) {
                try {
                    field = arg.getClass().getSuperclass().getDeclaredField(name);  // 如果没找到该名字的字段, 就到该类的父类中去找
                } catch (NoSuchFieldException ex) {
                    return null;    // 都没有返回null
                }
            }
            if (field == null) {
                return null;    // 如果没有字段, 返回null
            }
            field.setAccessible(true);
            Object o = field.get(arg);  // 有的话获取该字段对应的对象
            field.setAccessible(false);
            return o;
        } catch (IllegalAccessException e) {
            return null;
        }
    }
}
